/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "plugins/ecmascript/runtime/js_finalization_registry.h"
#include "plugins/ecmascript/runtime/js_invoker.h"
#include "plugins/ecmascript/runtime/js_tagged_value.h"
#include "plugins/ecmascript/runtime/js_weak_container.h"
#include "plugins/ecmascript/runtime/object_factory.h"
#include "plugins/ecmascript/runtime/internal_call_params.h"

namespace ark::ecmascript {
uint32_t JSFinalizationRegistry::GetLength() const
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->GetLength() / 3U;
}

JSTaggedValue JSFinalizationRegistry::GetObject(uint32_t idx)
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->Get(idx * 3U);
}

void JSFinalizationRegistry::SetObject(const JSThread *thread, uint32_t idx, JSTaggedValue obj)
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->Set(thread, idx * 3U, obj);
}

JSTaggedValue JSFinalizationRegistry::GetCallbackArg(uint32_t idx)
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->Get(idx * 3U + 1U);
}

void JSFinalizationRegistry::SetCallbackArg(const JSThread *thread, uint32_t idx, JSTaggedValue arg)
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->Set(thread, idx * 3U + 1U, arg);
}

JSTaggedValue JSFinalizationRegistry::GetToken(uint32_t idx)
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->Get(idx * 3U + 2U);
}

void JSFinalizationRegistry::SetToken(const JSThread *thread, uint32_t idx, JSTaggedValue token)
{
    return TaggedArray::Cast(GetCells().GetRawHeapObject())->Set(thread, idx * 3U + 2U, token);
}

void JSFinalizationRegistry::Register(const JSThread *thread, const JSHandle<JSFinalizationRegistry> &registry,
                                      const JSHandle<JSTaggedValue> &object, const JSHandle<JSTaggedValue> &callbackArg,
                                      const JSHandle<JSTaggedValue> &token)
{
    uint32_t length = registry->GetLength();
    uint32_t freeCellIdx = length;
    // Find a free triplet. The free triplet is a triplet where the object value is JSTaggedValue::Hole().
    bool found = false;
    for (uint32_t i = 0; i < length && !found; ++i) {
        JSHandle<JSTaggedValue> obj(thread, registry->GetObject(i));
        if (obj.GetTaggedValue().IsHole()) {
            found = true;
            freeCellIdx = i;
        }
    }
    if (!found) {
        // There is no free triplet. Expand the array.
        JSMutableHandle<TaggedArray> cells(thread, registry->GetCells());
        cells.Update(TaggedArray::SetCapacity(thread, cells, 3U * (length + 1)));
        registry->SetCells(thread, cells);
    }
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    JSHandle<JSWeakRef> objWeakRef = factory->NewWeakRef(JSHandle<JSObject>::Cast(object));
    registry->SetObject(thread, freeCellIdx, objWeakRef.GetTaggedValue());
    registry->SetCallbackArg(thread, freeCellIdx, callbackArg.GetTaggedValue());
    if (token->IsHole()) {
        registry->SetToken(thread, freeCellIdx, JSTaggedValue::Hole());
    } else {
        JSHandle<JSWeakRef> tokenWeakRef = factory->NewWeakRef(JSHandle<JSObject>::Cast(token));
        registry->SetToken(thread, freeCellIdx, tokenWeakRef.GetTaggedValue());
    }
}

bool JSFinalizationRegistry::Unregister(JSThread *thread, const JSHandle<JSFinalizationRegistry> &registry,
                                        const JSHandle<JSTaggedValue> &token)
{
    // Find all triplets with the matching token and replace them by JSTaggedValue::Hole
    bool found = false;
    uint32_t length = registry->GetLength();
    for (uint32_t i = 0; i < length; ++i) {
        if (!registry->GetToken(i).IsHole()) {
            JSHandle<JSWeakRef> t(thread, JSWeakRef::Cast(registry->GetToken(i).GetRawHeapObject()));
            if (JSTaggedValue::SameValue(t->GetReferent(), token.GetTaggedValue())) {
                registry->SetObject(thread, i, JSTaggedValue::Hole());
                registry->SetCallbackArg(thread, i, JSTaggedValue::Hole());
                registry->SetToken(thread, i, JSTaggedValue::Hole());
                found = true;
            }
        }
    }
    return found;
}

void JSFinalizationRegistry::CallCleanupCallback(JSThread *thread, JSHandle<JSFinalizationRegistry> registry)
{
    JSHandle<JSFunction> callback(thread, registry->GetCleanupCallback());
    JSHandle<JSTaggedValue> newTarget(thread, JSTaggedValue::Undefined());
    JSHandle<JSTaggedValue> global(thread, thread->GetGlobalObject());

    // Find all GC-ed objects and call FinalizationRegistry.callback for them.
    uint32_t length = registry->GetLength();
    for (uint32_t i = 0; i < length; ++i) {
        if (!registry->GetObject(i).IsHole()) {
            JSHandle<JSWeakRef> objWeakRef(thread, JSWeakRef::Cast(registry->GetObject(i).GetRawHeapObject()));
            if (objWeakRef->GetReferent().IsUndefined()) {
                JSHandle<JSTaggedValue> arg(thread, registry->GetCallbackArg(i));
                // Clear the triplet.
                registry->SetObject(thread, i, JSTaggedValue::Hole());
                registry->SetCallbackArg(thread, i, JSTaggedValue::Hole());
                registry->SetToken(thread, i, JSTaggedValue::Hole());

                if (arg->IsHole()) {
                    auto info = NewRuntimeCallInfo(thread, callback, global, JSTaggedValue::Undefined(), 0);
                    JSFunction::Call(info.Get());
                } else {
                    auto info = NewRuntimeCallInfo(thread, callback, global, JSTaggedValue::Undefined(), 1);
                    info->SetCallArgs(arg);
                    JSFunction::Call(info.Get());
                }
            }
        }
    }
}
}  // namespace ark::ecmascript
